/**
 * clone from https://github.com/Tencent/wepy
 * modified by Janus
 **/

export default {
  $isEmpty(obj) {
    return Object.keys(obj).length === 0;
  },

  $isEqual(a, b, aStack, bStack) {
    // Identical objects are equal. `0 === -0`, but they aren't identical.
    // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
    if (a === b) return a !== 0 || 1 / a === 1 / b;
    // `NaN`s are equivalent, but non-reflexive.
    if (a !== a) return b !== b;
    // A strict comparison is necessary because `null == undefined`.
    if (!a || !b) return a === b;
    // Exhaust primitive checks
    var type = typeof a;
    if (type !== 'function' && type !== 'object' && typeof b !== 'object') return false;
    return this.$isDeepEqual(a, b, aStack, bStack);
  },

  $isDeepEqual(a, b, aStack, bStack) {
    let self = this;
    // Compare `[[Class]]` names.
    var className = toString.call(a);
    if (className !== toString.call(b)) return false;
    switch (className) {
        // Strings, numbers, regular expressions, dates, and booleans are compared by value.
      case '[object RegExp]':
        // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
      case '[object String]':
        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
        // equivalent to `new String("5")`.
        return '' + a === '' + b;
      case '[object Number]':
        // `NaN`s are equivalent, but non-reflexive.
        // Object(NaN) is equivalent to NaN.
        if (+a !== +a) return +b !== +b;
        // An `egal` comparison is performed for other numeric values.
        return +a === 0 ? 1 / +a === 1 / b : +a === +b;
      case '[object Date]':
      case '[object Boolean]':
        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
        // millisecond representations. Note that invalid dates with millisecond representations
        // of `NaN` are not equivalent.
        return +a === +b;
      case '[object Symbol]':
        var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
        return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
    }

    var areArrays = className === '[object Array]';
    if (!areArrays) {
      if (typeof a !== 'object' || typeof b !== 'object') return a === b;

      // Objects with different constructors are not equivalent, but `Object`s or `Array`s
      // from different frames are.
      var aCtor = a.constructor, bCtor = b.constructor;
      if (aCtor !== bCtor && !(typeof aCtor === 'function' && aCtor instanceof aCtor &&
              typeof bCtor === 'function' && bCtor instanceof bCtor)
          && ('constructor' in a && 'constructor' in b)) {
        return false;
      }
    }
    // Assume equality for cyclic structures. The algorithm for detecting cyclic
    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.

    // Initializing stack of traversed objects.
    // It's done here since we only need them for objects and arrays comparison.
    aStack = aStack || [];
    bStack = bStack || [];
    var length = aStack.length;
    while (length--) {
      // Linear search. Performance is inversely proportional to the number of
      // unique nested structures.
      if (aStack[length] === a) return bStack[length] === b;
    }

    // Add the first object to the stack of traversed objects.
    aStack.push(a);
    bStack.push(b);

    // Recursively compare objects and arrays.
    if (areArrays) {
      // Compare array lengths to determine if a deep comparison is necessary.
      length = a.length;
      if (length !== b.length) return false;
      // Deep compare the contents, ignoring non-numeric properties.
      while (length--) {
        if (!self.$isEqual(a[length], b[length], aStack, bStack)) return false;
      }
    } else {
      // Deep compare objects.
      var keys = Object.keys(a), key;
      length = keys.length;
      // Ensure that both objects contain the same number of properties before comparing deep equality.
      if (Object.keys(b).length !== length) return false;
      while (length--) {
        // Deep compare each member
        key = keys[length];
        if (!(self.$has(b, key) && self.$isEqual(a[key], b[key], aStack, bStack))) return false;
      }
    }
    // Remove the first object from the stack of traversed objects.
    aStack.pop();
    bStack.pop();
    return true;
  },

  $has(obj, path) {
    if (toString.call(path) !== '[object Array]') {
      return obj && hasOwnProperty.call(obj, path);
    }
    var length = path.length;
    for (var i = 0; i < length; i++) {
      var key = path[i];
      if (!obj || !hasOwnProperty.call(obj, key)) {
        return false;
      }
      obj = obj[key];
    }
    return !!length;
  },

  $forEach(obj, fn) {
    if (obj === null || typeof obj === 'undefined') {
      return;
    }
    if (typeof obj !== 'object') {
      obj = [obj];
    }

    if (Array.isArray(obj)) {
      for (var i = 0, l = obj.length; i < l; i++) {
        fn.call(null, obj[i], i, obj);
      }
    } else {
      for (var key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          fn.call(null, obj[key], key, obj);
        }
      }
    }
  },

  $merge(/* obj1, obj2, obj3, ... */) {
    var result = {};

    function assignValue(val, key) {
      if (typeof result[key] === 'object' && typeof val === 'object') {
        result[key] = this.$merge(result[key], val);
      } else {
        result[key] = val;
      }
    }

    for (var i = 0, l = arguments.length; i < l; i++) {
      this.$forEach(arguments[i], assignValue);
    }
    return result;
  },

  $extend() {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length,
        deep = false;
    var self = this;

    // Handle a deep copy situation
    if (typeof target === 'boolean') {
      deep = target;

      // Skip the boolean and the target
      target = arguments[i] || {};
      i++;
    }

    // Handle case when target is a string or something (possible in deep copy)
    if (typeof target !== 'object' && !(typeof(target) === 'function')) {
      target = {};
    }

    // Extend jQuery itself if only one argument is passed
    if (i === length) {
      target = this;
      i--;
    }

    for (; i < length; i++) {

      // Only deal with non-null/undefined values
      if ((options = arguments[i])) {

        // Extend the base object
        for (name in options) {
          src = target[name];
          copy = options[name];

          // Prevent never-ending loop
          if (target === copy) {
            continue;
          }

          // Recurse if we're merging plain objects or arrays
          if (deep && copy && (self.$isPlainObject(copy) ||
                  (copyIsArray = Array.isArray(copy)))) {

            if (copyIsArray) {
              copyIsArray = false;
              clone = src && Array.isArray(src) ? src : [];

            } else {
              clone = src && self.$isPlainObject(src) ? src : {};
            }

            // Never move original objects, clone them
            target[name] = self.$extend(deep, clone, copy);

            // Don't bring in undefined values => bring undefined values
          } else {
            target[name] = copy;
          }
        }
      }
    }

    // Return the modified object
    return target;
  },

  $copy(obj, deep = false) {
    if (Array.isArray(obj)) {
      return this.$extend(deep, [], obj);
    } else if ('' + obj === 'null') {
      return obj;
    } else if (typeof (obj) === 'object') {
      return this.$extend(deep, {}, obj);
    } else
      return obj;
  },

  $isPlainObject(obj) {
    var proto, Ctor;

    // Detect obvious negatives
    // Use toString instead of jQuery.type to catch host objects
    if (!obj || Object.prototype.toString.call(obj) !== '[object Object]') {
      return false;
    }

    proto = Object.getPrototypeOf(obj);

    // Objects with no prototype (e.g., `Object.create( null )`) are plain
    if (!proto) {
      return true;
    }

    // Objects with prototype are plain iff they were constructed by a global Object function
    Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
    return typeof Ctor === 'function' && Object.prototype.hasOwnProperty.toString.call(Ctor) === Object.prototype.hasOwnProperty.toString.call(Object);
  },

  /**
   * resolve path
   * @param  {[string]} route ['/page/index']
   * @param  {[string]} url   ['./page2']
   * @return {[string]}       ['/page/page2']
   */
  $resolvePath(route, url) {
    if (!url)
      return route;
    if (url[0] === '/') { // /index
      url = url.substr(1);
      return this.$resolvePath('', url);
    }
    if (url[0] !== '.') { // index
      return this.$resolvePath(route, './' + url);
    }
    let current = route.split('/');
    if (url[0] === '.' && url[1] === '/') { // ./index
      url = url.substr(2);
      if (url[0] !== '.') {
        if (current.length)
          current[current.length - 1] = url;
        else
          current = [url];
        return current.length === 1 ? ('/' + current[0]) : current.join('/');
      }
      return this.$resolvePath(current.join('/'), url);
    }
    if (url[0] === '.' && url[1] === '.' && url[2] === '/') { // ../index || ....../index || ../../../index
      url = url.replace(/^\.*/ig, '');
      current.pop();
      return this.$resolvePath(current.join('/'), '.' + url);
    }
    if (url[0] === '.') {
      return this.$resolvePath(route, url.substr(1));
    }
  },
  /**
   * get url params
   * @param  {String} url index?a=1&b=2
   * @return {Object}     {a:1,b:2}
   */
  $getParams(url) {
    let rst = {};
    let quoteIndex = url.indexOf('?');

    if (quoteIndex !== -1) {
      let str = url.substr(quoteIndex + 1);
      let tmp;
      str.split('&').forEach(v => {
        tmp = v.split('=');
        rst[tmp[0]] = decodeURIComponent(tmp[1]);
      });
    }
    return rst;
  },
  /**
   * Hyphenate a camelCase string.
   */
  hyphenate(str) {
    return str
        .replace(/([^-])([A-Z])/g, '$1-$2')
        .replace(/([^-])([A-Z])/g, '$1-$2')
        .toLowerCase();
  },
  /**
   * Camelize a hyphen-delimited string.
   */
  camelize(str) {
    return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '');
  },

  isAbsoluteURL(url) {
    // A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
    // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
    // by any combination of letters, digits, plus, period, or hyphen.
    return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
  },

  combineURLs(baseURL, relativeURL) {
    return relativeURL
        ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
        : baseURL;
  },

  isExpire (expireTime) {
    let now = new Date().getTime() / 1000;
    return now >= expireTime;
  },

  isNull(obj) {
    return obj === null;
  },
};